連結: Day 5 - Colab
在進入今天的主題前, 我們需要了解 Variable Scope 和 Variable Expansion 是什麼東西, 才能更好的理解 CMake functions 的運作模式和 CMake 中很重要的 Modules 要如何使用, 並且避免不好的寫法
Variable scope, 可以理解為變數的 存活範圍 或是 可以被看見的範圍, 在這個範圍內建立的變數
來看看下面的例子
set(x 1)
block()
  set(x 2)
  set(y 3)
  message("inner x: ${x}")
  message("inner y: ${y}")
endblock()
message("outer x: ${x}")
message("outer y: ${y}")
set()
message()
message() 是用來將訊息印到 terminal 上即可block()
block() 的用法來看看上面的 message() 分別會輸出什麼結果
inner x: 2
inner y: 3
outer x: 1
outer y: 
可以看到, block() 裡面是新的 variable scope, 所以即使覆蓋了外層 copy 進來的 x 和 y 也不會影響
有幾種方法
block(PROPAGATE)
return([PROPAGATE vars...])
由於 回傳變數 或是 修改外層變數 的方法違背 CMake3.0 主打的 target-centered 設計理念
本系列不會詳細介紹, 如果有興趣可以參考官方文件~
Target-centered 的概念會在 [Day 8] Target 類型 介紹
在介紹 functions 前的最後一步, 要先來講講 Variable Expansion 是什麼
記得在 Day 4 有提到, 設定 normal variables 時 CMake 會把所有的值都轉成字串
有多個空白分隔的值的話, 則會用分號 ; 合併成一個字串
當我們用 ${varName} 取值的這個行為, 就叫做 Variable Expansion
也可以理解成, 將變數替換為他儲存的值, 所以也稱作 Variable Replacement
因此, 我們可以將一個變數賦值給另一個變數, 比如
set(name "ItHome2023")
set(it_home_name ${name})
message("name: ${name}") # name: ItHome2023
message("it_home_name: ${it_home_name}") # it_home_name: ItHome2023
很簡單
那麼, 有趣的問題來了, 如果空白分隔會被 CMake 用分號合併成字串
請問: 將用空白分隔的字串賦值給另一個變數時, 被賦值的變數是拿到多個值, 還是一個字串呢?
set(name "ItHome Ironman 2023")
set(it_home_name ${name})
message("name: ${name}") # name: ItHome Ironman 2023
message("it_home_name: ${it_home_name}") # it_home_name: ???
答案是
name: ItHome Ironman 2023
it_home_name: ItHome Ironman 2023
要記得, CMake 會先處理空白分隔的變數, 然後才進行 Variable Expansion
有了這個概念後, 我們就可以進入 Function 的世界了
記得上面提到的 Variable Scope 嗎? 由於使用每個 function 時, 都會建立新的 variable scope (就像 block())
所以我們可以用 functions 來封裝一些常見的邏輯, 而不會影響到使用 function 的人 (caller)
話不多說, 直接來看 function 怎麼寫
function(<name> [<arg1> ...])
  <commands>
endfunction()
name
arg1
commands
來看個例子
function(hello name)
  message("Hello ${name}!")
endfunction()
hello(Eric) # Hello Eric!
Function 的基本用法就這麼簡單, 只要定義 function name 和 argument 就好
但如果
下面就來介紹要如何達到這些目的
在 function 被呼叫時, CMake 會自動幫我們在新 varaible scope 建立幾個變數
ARGC
ARGV
ARGV# 取得對應變數ARGN
可以參照下面的例子會比較清楚
function(hello firstNamed surName)
  message("ARGN: ${ARGN}") # ARGN: unnamedVariable1;unnamedVariable2
  message("ARGV: ${ARGV}") # ARGV: eric;hung;unnamedVariable1;unnamedVariable2
  message("ARGV0: ${ARGV0}") # ARGV0: eric
endfunction()
hello(eric hung unnamedVariable1 unnamedVariable2)
那如果想要用 keyword arguments 呢?
這時候就需要 cmake_parse_arguments() 了!
cmake_parse_arguments()Note: CMake 3.7 開始有加入新的語法, 但由於 Colab 環境是用 CMake 3.27, 所以這邊不會介紹新的語法, 當然, 下面語法也適用 CMake 3.27 以上版本
有興趣的可以去官網查看 cmake_parse_arguments
cmake_parse_arguments(
  <prefix>
  <options> <one_value_keywords> <multi_value_keywords>
  <args>...
)
include(CMakeParseArguments)
CMakeParseArguments 是 CMake 提供的一個 module, 將他 include() 近來就可以使用裡面的各種變數include(moduleName) 指令會將 module 中的變數都加到現在的環境prefix
${prefix}
options
false, 相對的, 有給就會是 true
one_value_keywords
multi_value_keywords
args
一樣可以和下面例子對照, 注意在使用 func() 的時候, keyword arguments 不會受到傳入的順序影響
利用這個特性, 我們就可以寫出很有彈性的 function 了, 讚讚🎉🎉🎉
function(func)
  set(prefix FOO)
  set(options OPTION1 OPTION2)
  set(keywordOneValue KEYWORD_ONE_VALUE)
  set(keywordMultiValue KEYWORD_MULTI_VALUE)
  include(CMakeParseArguments)
  cmake_parse_arguments(
    ${prefix}
    "${options}"
    "${keywordOneValue}"
    "${keywordMultiValue}"
    ${ARGN} # args...
  )
  message("prefix: ${prefix}")
  message("Options: ")
  foreach(arg IN LISTS options)
    if(${prefix}_${arg})
      message(" ${arg} enabled")
    else()
      message(" ${arg} disabled")
    endif()
  endforeach()
  message("Keywords: ")
  foreach(arg IN LISTS keywordOneValue keywordMultiValue)
    message(" ${arg} = ${prefix}_${arg} = ${${prefix}_${arg}}")
  endforeach()
  message("args: ${ARGN}")
endfunction()
func(
  KEYWORD_ONE_VALUE keyword_one_value
  KEYWORD_MULTI_VALUE keyword multi value
  OPTION2
  rest arg1 arg2
)
會得到
prefix: FOO
Options: 
 OPTION1 disabled
 OPTION2 enabled
Keywords: 
 KEYWORD_ONE_VALUE = FOO_KEYWORD_ONE_VALUE = keyword_one_value
 KEYWORD_MULTI_VALUE = FOO_KEYWORD_MULTI_VALUE = keyword;multi;value
args: KEYWORD_ONE_VALUE;keyword_one_value;KEYWORD_MULTI_VALUE;keyword;multi;value;OPTION2;rest;arg1;arg2
到此, 我們已經能夠寫出相當有彈性的 function 了
接下來, 就該思考如何把這些棒棒的 functions 集中管理, 方便自己或是其他人使用了
所以, 就我們來看看 Modules 吧
到目前為止, 我們的專案已經有了 source directory (src) 和 build directory (build), 是時候加入新的資料夾了
通常 CMake modules 檔案會命名為 *.cmake, 並放在名為 cmake 的資料夾底下統一管理, 這樣就只需要將 CMake 的 predefined variableCMAKE_MODULE_PATH 設為該路徑, 就可以很輕易的找到該 module 了
我們的專案結構會長這樣
cmake-example/
├─ cmake/
├─ src/
├─ build/
├─ CMakeLists.txt
我們可以在 cmake 資料夾底下新增一個 module Func.cmake, 並將剛剛寫的 func 改寫在這裡
所以, 現在我們的專案架構會變成
cmake-example/
├─ cmake/
│  ├─ func.cmake 👈
├─ src/
├─ build/
├─ CMakeLists.txt
那麼, 由於 module 是不能直接執行的, 那要怎麼在 CMakeLists.txt 使用我們的 func 呢?
沒錯! 就像上面的 include(CMakeParseArguments) 一樣, 只要在 CMakeLists.txt 中用 include(Func) 即可, 不過為了讓 CMake 能知道我們把 modules 放在哪裡, 需要先將路徑加入 CMAKE_MODULE_PATH
CMakeLists.txt
cmake_minimum_required(VERSION 3.27)
project(ItHome2023)
list(APPEND CMAKE_MODULE_PATH "${CMAKE_CURRENT_SOURCE_DIR}/cmake")
include(Func)
func(
  KEYWORD_ONE_VALUE keyword_one_value
  KEYWORD_MULTI_VALUE keyword multi value
  OPTION2
  rest arg1 arg2
)
就是這麼簡單, 大家可以到 Colab 上執行看看結果和沒有 module 時是否相同
另外, 注意到了嗎? Module 的命名是對應其提供的 function 的
比如 CMakeParseArguments 包含 cmake_parse_arguments() function, 我們的 Func module 包含 func function
這並非硬性規定, 只是 CMake 的 conventions, 但個人建議依照這個原則去寫 module, 才能讓使用者或是未來的自己能 預期 這個 module 會提供哪些東西
學會了 function 和 module 的用法後, 離我們的 CMake 專案又更近一步了! 下一篇讓我們來看看到底什麼是 Generator 吧